package pri.damai.xiaowu.gateway.security;

import cn.hutool.core.util.URLUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import pri.damai.xiaowu.common.core.exception.GlobalException;
import pri.damai.xiaowu.common.core.util.DateUtils;
import pri.damai.xiaowu.common.security.config.SecurityUtil;
import pri.damai.xiaowu.common.security.config.XiaoWuSecurityProperties;
import pri.damai.xiaowu.common.security.constant.AuthConstant;
import pri.damai.xiaowu.common.security.model.AuthRole;
import pri.damai.xiaowu.common.security.token.RuiRuanAccessToken;
import pri.damai.xiaowu.gateway.enums.GatewayResultEnum;
import pri.damai.xiaowu.gateway.security.core.XiaoWuAuthenticationManager;
import pri.damai.xiaowu.gateway.security.model.SecurityUser;
import pri.damai.xiaowu.gateway.security.model.XiaoWuAuthenticationToken;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 从 token 中提取用户凭证
 *
 * @Desc 用于在请求之间保留上下文。
 * @Author DaMai
 * @Date 2021/3/23 15:26
 * 但行好事，莫问前程。
 */
@RefreshScope
@Component
@Slf4j
public class XiaoWuSecurityContextRepository implements ServerSecurityContextRepository {

    @Resource
    private XiaoWuAuthenticationManager xiaoWuAuthenticationManager;
    @Autowired
    RedisTemplate<String, Object> redisTemplate;
    @Resource
    SecurityUtil securityUtil;
    @Resource
    XiaoWuSecurityProperties securityProperties;
    @Value("${xiaowu.security.expiration-interval}")
    long expirationInterval;

    @Value("${temp-token}")
    String tempToken;

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        return Mono.empty();
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {

        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders httpHeaders = request.getHeaders();

        // 预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.empty();
        }

        // 配置了不登陆即可访问，直接放行。
        String path = request.getURI().getPath();
        boolean isNotLoginUrl = securityProperties.isNoLoginPass(path);
        if (isNotLoginUrl || path.contains("websocket") ) {
            return Mono.empty();
        }

//        String userId = httpHeaders.getFirst(HttpHeaderKeyConstant.USER_ID_KEY);
//        if (StringUtils.isNotBlank(userId)) {
//            return Mono.empty();
//        }

        // 获取 token
        String authorization = httpHeaders.getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.isBlank(authorization)) {
            throw new AuthenticationServiceException(GatewayResultEnum.UNAUTHORIZED.desc());
        }

        // 切割 token
        String token = this.getToken(authorization);

        // 获取 token 负载
        Claims claims = this.getTokenClaims(token);

        SecurityUser securityUser = this.covertClaimsToSecurityUser(claims);

        // 确认 token 有效性
        //checkToken(token, securityUser.getUserId());

        // 构建 Security 的认证凭据
        XiaoWuAuthenticationToken authToken = new XiaoWuAuthenticationToken(securityUser.getUserId(), securityUser, securityUser.getAuthorities());

        log.debug("从 token 中解析出的用户信息：{},构建的 Security 凭据:{}", securityUser, authToken);

        // 从请求头中删除token，并添加解析出来的信息
        ServerHttpRequest request1 = exchange.getRequest().mutate()
                .header(AuthConstant.USER_ID_KEY, securityUser.getUserId())
                .header(AuthConstant.USER_NAME_KEY, urlEncode(securityUser.getUsername()))
                .header(AuthConstant.ORG_ID_KEY, securityUser.getOrgId())
                .header(AuthConstant.ORG_NAME_KEY, urlEncode(securityUser.getOrgName()))
                .header(AuthConstant.ROLES_STRING_KEY, urlEncode(securityUser.getRolesStr()))
                //.headers(headers -> headers.remove(HttpHeaders.AUTHORIZATION))
                .build();
        exchange.mutate().request(request1).build();

        return xiaoWuAuthenticationManager
                .authenticate(authToken)
                .map(SecurityContextImpl::new);
    }

    /**
     * 校验Token正确性
     * @apiNote 附加的 token 有效性检验都在这里做
     * @param token
     * @return void
     * @author DaMai
     * @date 2021/4/2 10:46
     */
    private void checkToken(String token, String userId) {

        String tokenRedisKey = getTokenRedisKey(userId);

        RuiRuanAccessToken ruiRuanAccessToken = (RuiRuanAccessToken) redisTemplate.opsForValue()
                .get(tokenRedisKey);

        if (Objects.isNull(ruiRuanAccessToken) || !Objects.equals(ruiRuanAccessToken.getToken(), token)) {
            log.warn("Redis 中不存在此 Token。Token:{}", token);
            throw new GlobalException(GatewayResultEnum.INVALID_TOKEN);
        }

        // 增加 token 保活校验
        boolean isMoreThan = securityUtil.isMoreThanTimeInterval(ruiRuanAccessToken);
        if (isMoreThan) {
            redisTemplate.delete(tokenRedisKey);
            throw new GlobalException(GatewayResultEnum.INVALID_TOKEN);
        }

        // 设置最后一次调用时间
        ruiRuanAccessToken.setLastCallTime(DateUtils.asTimeStamp(LocalDateTime.now()));
        redisTemplate.opsForValue().set(tokenRedisKey, ruiRuanAccessToken, securityProperties.getExpiration(), TimeUnit.SECONDS);
    }

    /**
     * token 负载转换为 SecurityUser
     * @apiNote [附加描述]
     * @param claims
     * @return pri.damai.xiaowu.gateway.security.model.SecurityUser
     * @author DaMai
     * @date 2022/3/1 20:25
     */
    private SecurityUser covertClaimsToSecurityUser(Claims claims) {
        String userId = claims.get(AuthConstant.USER_ID_KEY, String.class);
        String username = claims.get(AuthConstant.USER_NAME_KEY, String.class);
        String orgId = claims.get(AuthConstant.ORG_ID_KEY, String.class);
        String orgName = claims.get(AuthConstant.ORG_NAME_KEY, String.class);
        String rolesStr = claims.get(AuthConstant.ROLES_STRING_KEY, String.class);
        List<AuthRole> authRoles = this.covertToAuthRoleList(rolesStr);

        SecurityUser securityUser = new SecurityUser();
        securityUser.setUserId(userId);
        securityUser.setUsername(username);
        securityUser.setOrgId(orgId);
        securityUser.setOrgName(orgName);
        securityUser.setRoleList(authRoles);
        return securityUser;
    }

    /**
     * 编码
     * @apiNote [附加描述]
     * @param param 参数
     * @return java.lang.String
     * @author DaMai
     * @date 2021/11/4 21:21
     */
    private String urlEncode(String param) {
        return URLUtil.encode(param);
    }

    /**
     * 获取存放Token的RedisKey
     * @apiNote [附加描述]
     * @param userId 用户Id
     * @return java.lang.String
     * @author DaMai
     * @date 2021/10/29 10:43
     */
    private String getTokenRedisKey(String userId) {
        return securityProperties.getTokenRedisKey() + userId;
    }

    /**
     * 获取Token
     * @apiNote [附加描述]
     * @param authorization 认证
     * @return java.lang.String
     * @author DaMai
     * @date 2021/12/24 9:34
     */
    private String getToken(String authorization) {
        String token;
        if (authorization.startsWith(AuthConstant.TOKEN_HEAD)) {
            token = authorization.substring(AuthConstant.TOKEN_HEAD.length());
            if (StringUtils.isBlank(token)) {
                log.warn("切割后 Token 为空。authorization:{},token:{}", authorization, token);
                return token;
            }
        } else {
            token = authorization;
        }
        return token;
    }

    /**
     * 转换为角色列表
     * @apiNote [附加描述]
     * @param rolesStr 角色字符串 1,2,3
     * @return java.util.List<com.ruiruan.security.model.AuthRole>
     * @author DaMai
     * @date 2021/12/24 9:37
     */
    private List<AuthRole> covertToAuthRoleList(String rolesStr) {
        List<AuthRole> list;
        if (StringUtils.isNotBlank(rolesStr)) {
            // 角色格式
            list = Arrays.stream(rolesStr.split(","))
                    .map(id -> new AuthRole().setId(id))
                    .collect(Collectors.toList());
        } else {
            list = new ArrayList<>();
        }
        return list;
    }

    /**
     * 获取 token 负载
     * @apiNote [附加描述]
     * @param token token
     * @return io.jsonwebtoken.Claims
     * @author DaMai
     * @date 2021/12/24 9:45
     */
    private Claims getTokenClaims(String token) {
        Claims claims = null;
        try {
            claims = securityUtil.getClaims(token);
        } catch (Exception e) {
            log.warn("TOKEN 解析异常。token:{},exception:{}", token, e.toString());
            GlobalException.throwException(GatewayResultEnum.INVALID_TOKEN);
        }
        return claims;
    }

}
